package org.example.filter;

import cn.lingyangwl.framework.tool.core.StringUtils;
import io.netty.buffer.ByteBufAllocator;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;


/**
 * 过滤器优先级不一定是最高，但是要在要获取body之前执行，然后后面在身份鉴定的等过滤器里面，获取到body
 *
 * 在后续Filter中，可以直接从ServerExchange中获取GatewayContext，就可以获取到缓存的数据，
 * 如果需要缓存其他数据，则可以根据自己的需求，添加到 GatewayContext中即可
 *
 * GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);
 *
 * 注意:
 *  json数据请求必须是 application/json 开头
 *
 * 如果你想要接收 content-type = application/x-www-form-urlencoded
 * 则你的springboot项目的controller接口应该使用 @RequestBody MultiValueMap< String, String > values 作为参数
 * 进行接收数据
 */
@Component
@Slf4j
@AllArgsConstructor
public class HttpBodyFilter implements GlobalFilter, Ordered {
    /**
     * default HttpMessageReader
     */
    private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 将请求路径和serviceId保存到网关上下文中
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        MultiValueMap<String, String> urlData = request.getQueryParams();
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        HttpHeaders headers = request.getHeaders();
        HttpMethod method = request.getMethod();
        long contentLength = headers.getContentLength();
        // 获取路由的目标URI
        URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        log.debug("请求路径:{},客户端远程IP地址:{},请求方法:{},目标URI:{}",
                path, remoteAddress, method, targetUri);

        try {

            // GatewayContext中目前只有一个String的path和String的requestBody
            GatewayContext gatewayContext = new GatewayContext();
            gatewayContext.setPath(path);
            gatewayContext.setUrlData(urlData);
            gatewayContext.setStartTime(System.currentTimeMillis());
            // GatewayContext.CACHE_GATEWAY_CONTEXT是一个常量定义，至于常量的值是什么，只要保证key值唯一即可，各位随便定义
            // 此处的作用为将我们需要的值放入Attributes中，以便在别的filter中使用，如果需要在别的地方使用，
            // 只需要exchange.getAttributes.get(GatewayContext.CACHE_GATEWAY_CONTEXT)即可
            exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext);
            if (mediaType == null || contentLength <= 0) {
                log.debug("请求既不是表单数据也不是json数据,不做任何处理 mediaType = [{}] contentLength = [{}]",mediaType,contentLength);
                return chain.filter(exchange);
            }

            if(MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON_UTF8.isCompatibleWith(mediaType)) {
                return readBody(exchange, chain,gatewayContext);
            }
            if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
                return readFormData(exchange, chain,gatewayContext);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return chain.filter(exchange);
    }


    /**
     * ReadFormData
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> readFormData(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){
        HttpHeaders headers = exchange.getRequest().getHeaders();
        return exchange.getFormData()
                .doOnNext(multiValueMap -> {
                    gatewayContext.setFormData(multiValueMap);
                    log.debug("[GatewayContext]Read FormData:{}",multiValueMap);
                }).then(Mono.defer(() -> {
                    Charset charset = headers.getContentType().getCharset();
                    charset = charset == null? StandardCharsets.UTF_8:charset;
                    String charsetName = charset.name();
                    MultiValueMap<String, String> formData = gatewayContext.getFormData();
                    // formData is empty just return
                    if(null == formData || formData.isEmpty()){
                        return chain.filter(exchange);
                    }
                    StringBuilder formDataBodyBuilder = new StringBuilder();
                    String entryKey;
                    List<String> entryValue;
                    try {
                        // repackage form data
                        for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
                            entryKey = entry.getKey();
                            entryValue = entry.getValue();
                            if (entryValue.size() > 1) {
                                for(String value : entryValue){
                                    formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&");
                                }
                            } else {
                                formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
                            }
                        }
                    } catch (UnsupportedEncodingException e){
                        //ignore URLEncode Exception
                    }
                    // substring with the last char '&'
                    String formDataBodyString = "";
                    if(formDataBodyBuilder.length() > 0){
                        formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
                    }
                    // get data bytes
                    byte[] bodyBytes =  formDataBodyString.getBytes(charset);
                    int contentLength = bodyBytes.length;
                    ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        // change content-length
                        @Override
                        public HttpHeaders getHeaders() {
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            if (contentLength > 0) {
                                httpHeaders.setContentLength(contentLength);
                            } else {
                                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                            }
                            return httpHeaders;
                        }

                        // read bytes to Flux<Databuffer>
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return DataBufferUtils.read(new ByteArrayResource(bodyBytes),new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),contentLength);
                        }
                    };
                    ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build();
                    log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString);
                    return chain.filter(mutateExchange);
                }));
    }

    /**
     * ReadJsonBody
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> readBody(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){
        // join the body
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    // read the body Flux<Databuffer>
                    DataBufferUtils.retain(dataBuffer);
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
                    // repackage ServerHttpRequest
                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                    // mutate exchage with new ServerHttpRequest
                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                    //read body string with default messageReaders
                    return ServerRequest.create(mutatedExchange, messageReaders)
                            .bodyToMono(String.class)
                            .doOnNext(objectValue -> {
                                gatewayContext.setCacheBody(objectValue);
                                log.debug("[GatewayContext]Read JsonBody:{}",objectValue);
                            }).then(chain.filter(mutatedExchange));
                });
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE + 1 ;
    }
}